<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>getset方法</title>
  <!-- --------------------------------------
    zhihu https://zhuanlan.zhihu.com/p/25003235?refer=e-mill
    双向绑定的简单实现——基于ES5对象的getter/setter机制

    API :　
    ES5提供了Object.defineProperty与Object.defineProperties两个API，
    允许我们为对象的属性增设getter/setter函数。
    利用它们，我们可以很方便地监听数据变更，并且在变更时加入自己的逻辑。
  --------------------------------------- -->
</head>
<body>

<div id="app">
  <form>
    <input type="text" v-model="count"/>
    <button type="button" v-click="increment">increment</button>
  </form>
  <p v-bind="count"></p>
</div>

<script>
  // Lue 实例
  function Lue(option) {
    this._init(option);
  }

  /**
   * 初始化函数
   * @param option
   * @private
   */
  Lue.prototype._init = function (option) {
    this.$options = option;
    this.$el = document.querySelector(option.el);
    this.$data = option.data;
    this.$methods = option.methods;

    // 与DOM绑定的数据对象集合
    // 每个成员属性有一个名为 _directives 的数组，用于在数据更新时触发更新 DOM 的各 directive
    this._binding = {};
    this._parseData(this.$data);
    this._parseFunc(this.$methods);

    this._compile(this.$el);
  }

  /**
   * 绑定数据对象的改造 - 对象属性重定义
   *    需要使用 Object.defineProperty 对 data 中的数据对象进行改造，
   *    添加 getter / setter 函数，使其在赋值和取值时能够被监听。
   * @param key
   * @param val
   */
  Lue.prototype.convert = function (key, val) {
    var binding = this._binding[key];
    Object.defineProperty(this.$data, key, {
      enumerable: true,
      configurable: true,
      get: function () {
        console.log('获取' + val);
        return val;
      },
      set: function (newVal) {
        console.log('更新' + newVal);
        if (val != newVal) {
          val = newVal;
          // 遍历该数据对象的 directive 并依次调用 update
          binding._directives.forEach(function (item) {
            item.update();
          })
        }
      }
    });
  }

  /**
   * 便利数据添加 get 和 set 方法。
   * @param obj
   * @private
   */
  Lue.prototype._parseData = function (obj) {
    var value = null;
    for (var key in obj) {
      // 排除原型链上的属性，仅仅遍历对象本身拥有的属性
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {
          _directives: []
        };
        value = obj[key];
        // 如果属性值为对象，则递归解析
        if (typeof value === 'object') {
          this._parseData(value);
        }
        this.convert(key, value);
      }
    }
  }

  /**
   * 遍历函数域，对绑定的函数进行改造
   * @param funcList
   * @private
   */
  Lue.prototype._parseFunc = function (funcList) {
    var _this = this;
    for (var key in funcList) {
      if (funcList.hasOwnProperty(key)) {
        var func = funcList[key];
        funcList[key] = (function () {
          return function () {
            func.apply(_this.$data, arguments);
          }
        })();
      }
    }
  };

  /**
   * 解析DOM的指令
   * @param root
   * @private
   */
  Lue.prototype._compile = function (root) {
    var _this = this;
    // 获取指定作用域下的所有子节点
    var nodes = root.children;
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      // 若该元素有子节点，则先递归编译其子节点
      if (node.children.length) {
        this._compile(node);
      }

      if (node.hasAttribute("v-click")) {
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute("v-click");
          var args = /\(.*\)/.exec(attrVal);
          // 如果函数带参数,将参数字符串转换为参数数组
          if (args) {
            args = args[0];
            attrVal = attrVal.replace(args, "");
            args = args.replace(/[\(|\)|\'|\"]/g, '').split(",");
          }
          else args = [];
          return function () {
            _this.$methods[attrVal].apply(_this.$data, args);
          }
        })();
      }

      if (node.hasAttribute(("v-model")) && node.tagName == "INPUT" || node.tagName == "TEXTAREA") {
        // 如果是input或textarea标签
        node.addEventListener("input", (function (key) {
          var attrVal = node.getAttribute("v-model");
          // 将value值的更新指令添加至_directives数组
          _this._binding[attrVal]._directives.push(new Directive(
              "input",
              node,
              _this,
              attrVal,
              "value"
          ))

          return function () {
            _this.$data[attrVal] = nodes[key].value;
          }
        })(i));
      }

      if (node.hasAttribute("v-bind")) {
        var attrVal = node.getAttribute("v-bind");
        //将innerHTML的更新指令添加至_directives数组
        _this._binding[attrVal]._directives.push(new Directive(
            "text",
            node,
            _this,
            attrVal,
            "innerHTML"
        ))
      }


    }
  };


  // Directive的作用就是建立一个DOM节点和对应数据的映射关系
  function Directive(name, el, vm, exp, attr) {
    this.name = name;         // 指令名称，例如文本节点，该值设为"text"
    this.el = el;             // 指令对应的DOM元素
    this.vm = vm;             // 指令所属Lue实例
    this.exp = exp;           // 指令对应的值，本例如 "count"
    this.attr = attr;         // 绑定的属性值，本例为 "innerHTML"

    this.update();            // 首次绑定时更新
  }

  Directive.prototype.update = function () {
    // 更新DOM节点的预设属性值
    this.el[this.attr] = this.vm.$data[this.exp];
  };


  // ------------------------------------------------------------- //

  // 使用类似Vue的语法创建一个双向绑定的实例
  var app = new Lue({
    el: "#app",
    data: {
      count: 0,
    },
    methods: {
      increment: function () {
        this.count++;
      }
    }
  });

</script>
</body>
</html>